// Sage Android Client.
// Connect to a Sage server, remotely execute commands and display results.
//
// Copyright (C) 2010, Harald Schilly <harald.schilly@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package org.sagemath.android;

import org.sagemath.android.interacts.AbstractInteract;
import org.sagemath.android.interacts.ExecuteCommand;
import org.sagemath.android.interacts.Plot;
import org.sagemath.android.interacts.SliderTest2;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.GestureDetector.OnGestureListener;
import android.view.View.OnClickListener;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewFlipper;
import android.widget.AdapterView.OnItemSelectedListener;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * Main Activity of the Sage Android application.
 * 
 * @author Harald Schilly
 */
final public class SageAndroid extends Activity implements OnGestureListener {
  private static final int UI_STATE = 0;
  
  private static final int UI_OUTPUT = 1;
  
  @SuppressWarnings("unused")
  private static final int[] CONN_COL = new int[] {
      Color.RED, Color.YELLOW, Color.GREEN
  };
  
  private LayoutInflater layoutInflater = null;
  
  TextView output = null; // , status = null;
  
  ViewFlipper vf = null;
  
  Animation inLeft, inRight, outLeft, outRight, fadeIn, fadeOut;
  
  // we talk with the sage connection service through its API
  private CalculationAPI api = null;
  
  /**
   * returns the CalculationAPI for the service (could be null)
   * 
   * @return
   */
  final public CalculationAPI getCalculationAPI() {
    return api;
  }
  
  final public Context getContext() {
    return SageAndroid.this;
  }
  
  private GestureDetector gestureDetector;
  
  // TODO remove it, just for development mockup
  private int stored_i = -1;
  
  private EditText txtFx;
  
  private AbstractInteract currentInteract = null;
  
  /**
   * Starting point of the whole Application:
   * <ol>
   * <li>We define an instance of {@link Global} and inject dependencies.</li>
   * <li>Start a background Service to handle the connection with Sage, i.e.
   * poll to keep it alive and wait for results. Details will depend on Sage's
   * Server API. From the perspective of this application, everything is done
   * via calls to the {@link CalculationAPI} and callbacks via
   * {@link CalculationListener}. UI updates happen in the local Handler.</li>
   * <li>There is a collection of "Interacts" to query the server in a certain
   * way. This is still unclear ...</li>
   * </ol>
   */
  @Override
  final public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
    requestWindowFeature(Window.FEATURE_RIGHT_ICON);
    setContentView(R.layout.main);
    // store a reference to me globally, a bit hackish
    Global.setContext(this);
    
    // call the background service, we can call this as often as we want
    // the intent must match the intent-filter in the manifest
    Intent intent = new Intent(SageConnection.class.getName());
    startService(intent);
    // the connection service does the wiring
    bindService(intent, sageConnnectionService, Context.BIND_AUTO_CREATE);
    
    // dynamic titlebar
    updateTitlebar(SageConnection.DISCONNECTED, "");
    
    // locally store the ui objects
    output = (TextView) findViewById(R.id.output);
    // status = (TextView) findViewById(R.id.status);
    vf = (ViewFlipper) findViewById(R.id.MainVF);
    
    // load the animations
    inLeft = AnimationUtils.makeInAnimation(this, true);
    inRight = AnimationUtils.makeInAnimation(this, false);
    outLeft = AnimationUtils.makeOutAnimation(this, false);
    outRight = AnimationUtils.makeOutAnimation(this, true);
    fadeOut = AnimationUtils.loadAnimation(this, R.anim.fade_out);
    fadeOut = AnimationUtils.loadAnimation(this, R.anim.fade_in);
    
    // registers the gesture detector with this view and ourself since we also
    // listen
    gestureDetector = new GestureDetector(this, this);
    
    layoutInflater = LayoutInflater.from(SageAndroid.this);
    
    Button testing = (Button) findViewById(R.id.btnTest);
    testing.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        try {
          api.setServer(new Server("http://www.bogusserver.org/", "admin", "password"));
          stored_i++;
          api.calculate(Long.toString(stored_i));
        } catch (RemoteException re) {
          output.setText("calculate remote exeception: \n" + re);
        }
      }
    });
    
    txtFx = (EditText) findViewById(R.id.txtFxTest);
    
    Button btnFxTest = (Button) findViewById(R.id.btnFxTest);
    btnFxTest.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        Intent intent = new Intent(SageAndroid.this, org.sagemath.android.fx.FunctionEditor.class);
        intent.putExtra("func", txtFx.getText().toString());
        intent.putExtra("start", txtFx.getSelectionStart());
        intent.putExtra("end", txtFx.getSelectionEnd());
        startActivityForResult(intent, Global.FUNC_EDIT);
      }
    });
  }
  
  /**
   * Listen to results from a started FunctionEditor activity.
   */
  @Override
  final protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // See which child activity is calling us back.
    switch (requestCode) {
      case Global.FUNC_EDIT:
        if (resultCode == RESULT_OK) {
          Bundle bundle = data.getBundleExtra("data");
          String func = bundle.getString("func");
          Log.d(Global.TAG, "func = " + func);
          txtFx.setText(func);
          txtFx.setSelection(bundle.getInt("start"), bundle.getInt("end"));
          txtFx.requestFocus();
        }
      default:
        break;
    }
  }
  
  /**
   * Helper
   * 
   * @see wireButton
   */
  final private void registerInteracts() {
    wireButton(R.id.btnActCmd, ExecuteCommand.class);
    wireButton(R.id.btnSlider, SliderTest2.class);
    wireButton(R.id.btnPlotInteract, Plot.class);
    wireButton(R.id.btnSystem, org.sagemath.android.interacts.System.class);
  }
  
  final private Map<Class<? extends AbstractInteract>, AbstractInteract> interactCache = new HashMap<Class<? extends AbstractInteract>, AbstractInteract>();
  
  /**
   * Helper to wire buttons with interact views.
   * 
   * @param id
   * @param interactView
   */
  final private void wireButton(final int id, final Class<? extends AbstractInteract> clsInteract) {
    // we only accept classes that extend the {@link AbstractInteract} abstract
    // class and when the button is pressed, we either use a cached object or
    // instantiate a new one.
    // this postpones the instantiation as long as possible and when opening the
    // same interact again the current state is preserved.
    ((Button) findViewById(id)).setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        AbstractInteract interact = null;
        
        if (!interactCache.containsKey(clsInteract)) {
          try {
            interact = clsInteract.newInstance();
            interactCache.put(clsInteract, interact);
          } catch (Exception ex) {
            Log.d(Global.TAG, ex.getLocalizedMessage(), ex);
          }
        } else {
          interact = interactCache.get(clsInteract);
          interact.reset();
        }
        
        interact.addCalcListener();
        showVF(interact);
      }
    });
  }
  
  /**
   * Helper to update the Titlebar.
   */
  final private void updateTitlebar(int status, String text) {
    // fyi, for more customization we need our own titlebar, that's possible
    setTitle(getString(R.string.app_name) + " " + text);
    // setTitleColor(CONN_COL[status]);
    
    switch (status) {
      case SageConnection.DISCONNECTED:
        setFeatureDrawableResource(Window.FEATURE_RIGHT_ICON, R.drawable.icon_conn_0);
        break;
      case SageConnection.CONNECTING:
        setFeatureDrawableResource(Window.FEATURE_RIGHT_ICON, R.drawable.icon_conn_1);
        break;
      case SageConnection.CONNECTED:
        setFeatureDrawableResource(Window.FEATURE_RIGHT_ICON, R.drawable.icon_conn_2);
        break;
      default:
        Toast.makeText(this, "Unknown State " + status, Toast.LENGTH_SHORT).show();
    }
  }
  
  /**
   * for the button's onClick, to show the view
   * 
   * @param v
   */
  final private void showVF(AbstractInteract ai) {
    vf.setInAnimation(fadeIn);
    vf.setOutAnimation(fadeOut);
    
    if (currentInteract != null) {
      currentInteract.removeCalcListener();
    }
    
    currentInteract = ai;
    
    // make sure that there is only the main view
    for (int cc; (cc = vf.getChildCount()) >= 2;) {
      // TODO unregister calculation listener
      vf.removeViewAt(cc - 1);
    }
    // add the interact and show it
    vf.addView(ai.getView());
    vf.showNext();
  }
  
  /**
   * called when application is restored - that's not persistence!
   */
  @Override
  final protected void onRestoreInstanceState(Bundle icicle) {
    stored_i = icicle.getInt("i", 0);
  }
  
  /**
   * called when application should save it's current state - that's not
   * persistence!
   */
  @Override
  final protected void onSaveInstanceState(Bundle icicle) {
    icicle.putInt("i", stored_i);
  }
  
  /**
   * Android lifecycle, we are back to front after a short break.
   */
  @Override
  final protected void onResume() {
    super.onResume();
    try {
      if (api != null) {
        // tell the service that we will see results on the screen
        api.setShowNotification(false);
      }
    } catch (RemoteException ex) {
      
    }
  }
  
  /**
   * Android lifecycle, we are currently hidden behind another window.
   */
  @Override
  final protected void onPause() {
    super.onPause();
    try {
      // tell the service to show notifications in the top bar
      if (api != null) {
        api.setShowNotification(true);
      }
    } catch (RemoteException ex) {
      
    }
  }
  
  /**
   * Android OS destroys us
   */
  @Override
  final protected void onDestroy() {
    super.onDestroy();
    try {
      if (api != null) {
        api.setShowNotification(true);
        api.removeCalcListener(calcListener);
      }
      unbindService(sageConnnectionService);
    } catch (Throwable t) {
      // catch any issues, typical for destroy routines
      // even if we failed to destroy something, we need to continue
      // destroying
      Log.w(Global.TAG, "Failed to unbind from the service", t);
    }
    Log.i(Global.TAG, "onDestroy()");
  }
  
  /**
   * this is called when the service is connected or disconnected.
   * we create the api interface here which is used to communicate with
   * the service.
   */
  final private ServiceConnection sageConnnectionService = new ServiceConnection() {
    @Override
    final public void onServiceConnected(ComponentName name, IBinder service) {
      // that's how we get the client side of the IPC connection
      api = CalculationAPI.Stub.asInterface(service);
      Global.setCalculationAPI(api);
      try {
        api.setShowNotification(false);
        api.addConnListener(connListener);
        api.addCalcListener(calcListener);
        
        // TODO remove this below
        // vf.removeViewAt(vf.getChildCount() - 1);
        // vf.addView(interacts.getSliderTest());
        
      } catch (RemoteException e) {
        Log.e(Global.TAG, "Failed to add listener", e);
      }
      Log.i(Global.TAG, "Service connection established");
      
      registerInteracts();
    }
    
    @Override
    final public void onServiceDisconnected(ComponentName name) {
      Log.i(Global.TAG, "Service connection closed: " + name.toShortString());
      api = null;
      Global.setCalculationAPI(null);
    }
  };
  
  /**
   * our little helper to update the ui via messages when updates happen on
   * other threads. it is bound to this thread and used for callbacks from the
   * service to the main application.
   */
  final private Handler handler = new Handler() {
    
    @Override
    public void handleMessage(Message msg) {
      msg.getData().getBoolean("status");
      switch (msg.what) {
        case UI_STATE:
          updateTitlebar((Integer) msg.obj, "");
          break;
        case UI_OUTPUT:
          output.setText("RECV: " + msg.obj);
          break;
        default:
          super.handleMessage(msg);
      }
    }
  };
  
  /**
   * this is called by the remote service to tell us about the calculation
   * result by giving us back the {@link Calculation} object with hopefully
   * some content in the result field.
   */
  final private CalculationListener.Stub calcListener = new CalculationListener.Stub() {
    @Override
    final public boolean handleCalculationResult(Calculation cr) throws RemoteException {
      handler.sendMessage(handler.obtainMessage(UI_OUTPUT, cr.getResult()));
      // false means that other listeners should also be informed about this
      return false;
    }
  };
  
  /**
   * this is called by the remote service to tell us about the connection
   * state.
   */
  final private ConnectionListener.Stub connListener = new ConnectionListener.Stub() {
    @Override
    final public void handleConnectionState(int i) throws RemoteException {
      handler.sendMessage(handler.obtainMessage(UI_STATE, i));
    }
  };
  
  /**
   * This is part of the App lifecycle. It is called when the OS wants to get
   * rid of us. Make the transient data persistent now!
   */
  @Override
  final protected void onStop() {
    super.onStop();
    Global.saveSettings();
  }
  
  /**
   * This is called when the dedicated back button is pressed.
   */
  @Override
  final public void onBackPressed() {
    flipViewFlipper(Global.FLIP);
  }
  
  /**
   * This either displays the last interact again (if there is a
   * current one) or just goes back to the main view in the view
   * flipper.
   */
  final private void flipViewFlipper(final int dir) {
    // check if there is something to show
    if (currentInteract != null) {
      // child 0 is main view
      if (vf.getDisplayedChild() == 0) {
        if (dir == Global.FLIP || dir == Global.RIGHT) {
          vf.setInAnimation(inRight);
          vf.setOutAnimation(outLeft);
          vf.setDisplayedChild(1);
        }
      } else {
        // case when interact is currently displayed
        if (dir == Global.FLIP || dir == Global.LEFT) {
          vf.setInAnimation(inLeft);
          vf.setOutAnimation(outRight);
          vf.setDisplayedChild(0);
        }
      }
    }
  }
  
  /**
   * This is called when we press the menu button on the device.
   * 
   * @see res/menu/main_menu.xml
   */
  @Override
  final public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.main_menu, menu);
    return true;
  }
  
  /**
   * This is called a button in the options menu is pressed.
   * This works when showing the main menu and when showing
   * an interact. The "id" is used to identify the selection.
   */
  @Override
  final public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
      case R.id.MenuSettingsDialog:
        showSettingsDialog();
        return true;
      case R.id.MenuQuit:
        this.finish();
        return true;
      default:
        return super.onOptionsItemSelected(item);
    }
  }
  
  /**
   * called via the menu to show the settings dialog
   */
  final private void showSettingsDialog() {
    // TODO encapsulate this in it's own class.
    final View textEntryView = layoutInflater.inflate(R.layout.settings_dialog, null);
    
    final AlertDialog dlg = new AlertDialog.Builder(SageAndroid.this)
        .setTitle("Server Credentials").setView(textEntryView)

        .setPositiveButton("Connect", new DialogInterface.OnClickListener() {
          @Override
          public void onClick(DialogInterface dialog, int whichButton) {
            Toast.makeText(SageAndroid.this, "Connect ...", Toast.LENGTH_LONG).show();
            
            AlertDialog d = (AlertDialog) dialog;
            
            EditText pe = (EditText) d.findViewById(R.id.PasswordEdit);
            EditText ue = (EditText) d.findViewById(R.id.UsernameEdit);
            EditText se = (EditText) d.findViewById(R.id.ServerEdit);
            
            Server s = new Server(se.getText().toString(), ue.getText().toString(), pe.getText()
                .toString());
            Global.setServer(s);
            Global.saveSettings();
          }
        })

        .setNeutralButton("Test", new DialogInterface.OnClickListener() {
          @Override
          public void onClick(DialogInterface dialog, int whichButton) {
            // dialog.dismiss();
            ProgressDialog pd = new ProgressDialog(getContext(), ProgressDialog.STYLE_SPINNER);
            pd.setMessage("Connecting...");
            pd.setButton(ProgressDialog.BUTTON_NEGATIVE, "Cancel",
                new DialogInterface.OnClickListener() {
                  
                  @Override
                  public void onClick(DialogInterface dialog, int which) {
                    dialog.dismiss();
                  }
                });
            pd.show();
          }
        })

        .setNegativeButton("Delete", new DialogInterface.OnClickListener() {
          @Override
          public void onClick(DialogInterface dialogMain, int whichButton) {
            
            final AlertDialog confirmDelete = new AlertDialog.Builder(getContext()) //
                .setIcon(android.R.drawable.ic_dialog_alert) //
                .setTitle("Confirm Delete") //
                .setMessage("Really delete " + Global.getServer().server) //
                .setPositiveButton("Yes", new DialogInterface.OnClickListener() {
                  
                  @Override
                  public void onClick(DialogInterface dialog, int which) {
                    Global.servers.remove(Global.getServer());
                  }
                  
                }) //
                .setNegativeButton("No", null).show();
          }
        }).create();
    
    dlg.show();
    
    // ~~~ Set User/Password
    
    EditText ue = (EditText) dlg.findViewById(R.id.UsernameEdit);
    ue.setText(Global.getUser());
    EditText pe = (EditText) dlg.findViewById(R.id.PasswordEdit);
    pe.setText(Global.getPassword());
    
    // ~~~ start Server Spinner
    
    final Spinner serverSpinner = (Spinner) dlg.findViewById(R.id.ServerSpinner);
    final ArrayList<Server> serverList = new ArrayList<Server>(Global.servers);
    final ArrayAdapter<Server> adapter = new ArrayAdapter<Server>(this,
        android.R.layout.simple_spinner_item, serverList);
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    serverSpinner.setAdapter(adapter);
    
    if (Global.getServer() != null) {
      int selidx = -1;
      for (int i = 0; i < adapter.getCount(); i++) {
        if (Global.getServer().equals(adapter.getItem(i))) {
          selidx = i;
          break;
        }
      }
      if (selidx < 0) {
        adapter.add(Global.getServer());
        serverSpinner.setSelection(adapter.getCount());
      } else {
        serverSpinner.setSelection(selidx);
      }
    }
    
    serverSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
      
      @Override
      final public void onItemSelected(AdapterView<?> arg0, View arg1, int position, long arg3) {
        final Server s = serverList.get(position);
        Global.setServer(s);
        EditText se = (EditText) dlg.findViewById(R.id.ServerEdit);
        se.setText(s.server);
      }
      
      @Override
      final public void onNothingSelected(AdapterView<?> arg0) {
        
      }
      
    });
    // ~~~~ end Server Spinner
    
  }
  
  // ~~~~ Touch Event business for the OnGestureListener ~~~~
  
  @Override
  final public boolean onTouchEvent(MotionEvent event) {
    return gestureDetector.onTouchEvent(event);
  }
  
  @Override
  final public boolean onDown(MotionEvent e) {
    return false;
  }
  
  @Override
  final public boolean onFling(MotionEvent arg0, MotionEvent arg1, float dx, float dy) {
    // info: dx/dy is measured in pixel/second. that's not a distance.
    // silly threshold
    if (Math.hypot(dx, dy) > 100) {
      if (dx > 200 && Math.abs(dy) < 50) { // right
        flipViewFlipper(Global.RIGHT);
      }
      if (dx < -200 && Math.abs(dy) < 50) { // left
        flipViewFlipper(Global.LEFT);
      }
    }
    return true;
  }
  
  @Override
  final public void onLongPress(MotionEvent arg0) {
  }
  
  @Override
  final public boolean onScroll(MotionEvent arg0, MotionEvent arg1, float arg2, float arg3) {
    return false;
  }
  
  @Override
  final public void onShowPress(MotionEvent arg0) {
    
  }
  
  @Override
  final public boolean onSingleTapUp(MotionEvent arg0) {
    return false;
  }
}
